package room

import (
	"context"
	"fmt"
	"sync"
	"time"

	"gitee.com/lichenxin/grpc-consul-server/pkg/consul"
	"gitee.com/lichenxin/grpc-consul-server/pkg/pb"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/connectivity"
)

var (
	roomClient     *Client
	roomClientOnce sync.Once

	// ErrServiceUnavailable 服务不可用
	ErrServiceUnavailable = errors.New("Service Unavailable")
	// ErrServiceConnectionFailed 服务连接失败
	ErrServiceConnectionFailed = errors.New("Service connection failed")
)

// Result 匹配结果
type Result struct {
	RoomID      int64
	MemberCount int64
	IsNew       bool
}

// Client 客户端
type Client struct {
	conn    *grpc.ClientConn
	l       *logrus.Entry
	period  time.Duration
	timeout time.Duration
}

// GetClient 获取客户端
func GetClient() *Client {
	roomClientOnce.Do(func() {
		roomClient = &Client{
			period:  time.Second,
			timeout: time.Second,
			l:       logrus.WithField("channel", "client"),
		}
		go roomClient.start()
	})
	return roomClient
}

func (s *Client) start() {
	for {
		time.Sleep(s.period)

		if s.conn != nil && s.conn.GetState() == connectivity.Ready {
			continue
		}

		conn, err := s.reconnect()
		if err != nil {
			s.l.WithError(err).Error("client reconnect")
			continue
		}

		s.conn = conn
	}
}

func (s *Client) connection() (*grpc.ClientConn, error) {
	if s.conn != nil {
		return s.conn, nil
	}

	conn, err := s.reconnect()
	if err != nil {
		return nil, errors.WithStack(ErrServiceConnectionFailed)
	}

	s.conn = conn
	return conn, nil
}

func (s *Client) reconnect() (*grpc.ClientConn, error) {
	ss, err := consul.GetHealthServers(ServerName)
	if err != nil {
		return nil, errors.Wrap(err, "get servers")
	}

	for _, v := range ss {
		addr := fmt.Sprintf("%s:%d", v.Address, v.Port)
		s.conn, err = grpc.Dial(addr, grpc.WithInsecure())
		if err != nil {
			return nil, errors.WithStack(err)
		}
		s.l.WithField("address", addr).Info("connect success")
		return s.conn, nil
	}

	return nil, errors.WithStack(ErrServiceUnavailable)
}

// Join 加入
func (s *Client) Join(ctx context.Context, userID int64) (Result, error) {
	conn, err := s.connection()
	if err != nil {
		return Result{}, err
	}

	var cancel context.CancelFunc
	if _, ok := ctx.Deadline(); !ok {
		ctx, cancel = context.WithTimeout(ctx, s.timeout)
		defer cancel()
	}

	rs, err := pb.NewRoomClient(conn).Join(ctx, &pb.JoinRoomRequest{
		UserID: userID,
	})
	if err != nil {
		return Result{}, errors.WithStack(err)
	}

	return Result{
		RoomID:      rs.RoomID,
		MemberCount: rs.MemberCount,
		IsNew:       rs.IsNew,
	}, nil
}
